Skip to content

fix(objectql,platform-objects): 空库自证改在本次启动写完数据之后 —— 一次启动不能证明它随即违反的契约 (#4769) - #4794

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4769-adr0104-attest-after-seed
Aug 3, 2026
Merged

fix(objectql,platform-objects): 空库自证改在本次启动写完数据之后 —— 一次启动不能证明它随即违反的契约 (#4769)#4794
os-zhuang merged 2 commits into
mainfrom
claude/issue-4769-adr0104-attest-after-seed

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #4769

问题

全新部署第一次 pnpm dev 全绿(130 rows,0 ERROR),第二次启动开始永久 10 条 ERROR,10 条种子记录写不进去。数据没变、代码没变,只是重启了一次 —— 被拒的正是首启自己写进去的数据。

根因不是哪个值算错了,是顺序反了sys_migration 里那两行带着 {"attested":"datastore-created-empty"} 的自证写在 kernel:ready,而同一次启动的 seed 还在往库里写行。「空库 ⇒ 没有历史值」这个推理的前提是没有数据可写,它却恰恰写在即将写入 130 行之前:证书落笔那一刻是真的,一秒之后就不是了。首启在 warn-first 下把数据留下,之后每一次启动读到这张证书、进入 strict、拒掉前任写下的那批行。

按 PM 裁定取修法 1(推迟到首启 seed 之后 + 对落定的数据重新下结论)。修法 2 只是把 insert/update 的不对称抹平成一样松或一样严,不触及真正坏掉的东西:证据在被推翻之前就被记账

改了什么

一条不变量,三个位置:一次启动不能证明一个它在同一次启动里违反的契约。

① 写入时机 —— 等本次启动自己的数据落定。
自证从 kernel:ready 改为订阅 app:seeded(inline seed 的结算点,含超出 OS_INLINE_SEED_BUDGET_MS 后台跑完的那一半);不 seed 的 kernel 仍由 kernel:ready 兜底。两条路径进的是同一个幂等调用:先到的那个写行,后到的读到行就跳过。

② 写入前提 —— 先问这次启动放行过什么。
attestFreshDatastore 先读引擎的「已放行违规值」台账,任何被本次启动证伪的 migration id 不再自证;部署维持 warn-first(真实、可恢复),并在日志里指名是哪个 对象.字段、该跑哪条 os migrate 才能真正关闸。

台账由写路径产生:引擎在 warn-first 放行每一个不合形状的值时,用与 strict 模式完全相同的判定把它记下来。这是刻意的不对称 —— 证明一个部署干净需要扫全库,证伪只需要一个反例,而这个反例写路径本来就已经算出来了,所以它零成本、精确,且不可能与将来的强制判定漂移。

adr-0104-file-referencesadr-0104-value-shapes 各自独立判定:一个 cover 不合形状不牵连 location 那道闸,反之亦然(两条 migration 声称的是两件不同的事)。

③ 写入之后 —— 被推翻就撤销。
证书若在签发之后被本次启动推翻(操作员显式开了 OS_ALLOW_LAX_MEDIA_VALUES / OS_ALLOW_LAX_VALUE_SHAPES,或后台 seed 收尾晚于签发),引擎撤销它:verified_at 清空、blocking 记上、details 保留原 attested 再补一条 revoked 说明是哪个值推翻的。范围刻意收窄到本次启动亲手创建的库上的自证行 —— 扫过全库的真实迁移证据,不该被一次写入的观察推翻。

记忆化(PM 要求一并审查的第二张脸)

结论:会脱节,是同一个 bug 的第二张脸,已在本 PR 一并修。

首启之所以「看起来是绿的」,一半靠的就是进程内正好缓存了 falsesys_migration 在 kernel init 期间才注册,而第一条写完全可能赶在它之前 —— 那一次读根本没读到账本,却被当成结论冻结了一整个进程的姿态。于是同一份代码、同一份数据,启动姿态取决于「哪条写先跑」。

现在区分两种否定:

  • 问过了,账本说不(没有行 / verified_at 为空 / blocking 非零)—— 是结论,照旧缓存,想立刻生效仍走 invalidateDataMigrationFlags();
  • 根本问不到(sys_migration 未注册、查询抛错)—— 依旧答 false(问不到的闸必须关着),但不记住,下一次写再问一次。

代价是账本存在之前每次写多一次 registry 查表(在任何查询之前就短路),账本可读之后即止。

kernel:bootstrapped 上那条 ADR-0104 建议行同理改为直接读账本 —— 否则一个刚刚自证成功的新部署,会被告知去跑一条它已经不需要跑的迁移。

测试

回归测试按 PM 的验收标准写成**「两次启动」**,不是单次:两个 engine 跑同一个 store,第二个 engine 的 driver 报告表已存在(wasDatastoreCreatedFromEmpty() === false)。单次启动的测试根本看不见这个缺陷 —— 坏的构建里首启也是绿的。

packages/objectql/src/adr0104-attestation-evidence.test.ts(新增 8 条)覆盖:反例按 migration id 记账、两道闸互不牵连、干净启动不留反例、两次启动同一数据集第二次不得拒绝首次写入的数据、以及它的对照组(没被推翻的证书在第二次启动照常强制,说明修的是「未获证」而不是「关掉了强制」)、非本次创建的库永不撤销、记忆化重问。

packages/platform-objects/src/{system/migration-flag,plugin}.test.ts(新增 8 条)覆盖:被证伪的 id 不自证且日志给出处方、只拒被证伪的那一条、订阅 app:seeded、两次 pass 幂等。

反向验证过:把源码 stash 掉、只留测试文件跑,8 条里 6 条失败(含「两次启动」与记忆化两条),确认它们真的钉住了这个回归。

packages/objectql          Test Files 108 passed   Tests 1693 passed
packages/platform-objects  Test Files   9 passed   Tests  266 passed
typecheck (两个包)          Done / Done
eslint(改动文件)            clean
check-adr-anchors           OK (17 anchored file(s))

范围

改动落在 packages/objectqlpackages/platform-objects 两个包:自证的写入者与时机本来就在 platform-objects(#4243 把它从 service-storage 移过去的),不动它无法修「时机」这一半;两个包都不在本轮并行车道上。

留在门外的两个窗口(已另立 issue,未在本 PR 修)

🤖 Generated with Claude Code

https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny

claude added 2 commits August 3, 2026 06:52
…'s own data, not on the emptiness it remembers (#4769)

A store created from empty recorded both ADR-0104 migrations as verified at
`kernel:ready`, while the same boot was still seeding rows whose values
contradict them. The certificate was true when written and false a second
later: the first boot ran warn-first and kept the data, every later boot read
the certificate, enforced it, and rejected the rows its predecessor had
written. Same data, same code, one restart.

Three changes, one invariant — a boot may not prove a contract it violates in
that same boot:

- The attestation waits for `app:seeded` (the inline seed's settle point,
  background continuation included), with `kernel:ready` as the backstop for
  kernels that never seed. Both enter the same idempotent call.
- `attestFreshDatastore` consults the engine's tally of ADMITTED value-shape
  violations first and declines any migration id this boot has already
  contradicted, naming the object.field and the command that closes the gate.
  The two ids are judged independently — a bad `cover` does not sink the
  `location` gate. The engine records those admissions from the warn-first
  path with the exact predicate strict mode uses, so certifying still needs a
  scan while refuting needs the one counterexample the write already computed.
- A certificate contradicted AFTER it was issued is revoked from the write
  path that contradicted it, scoped to creation attestations on a store this
  boot created; evidence produced by a real migration run is never rewritten.

Also fixes the memoized flag read, the other half of why the first boot looked
green: "the ledger says no" is now distinguished from "the ledger could not be
asked" (sys_migration not registered yet, or the query threw). Both keep the
gate closed, only the former is remembered, so one unlucky early write no
longer freezes a whole boot's posture. The `kernel:bootstrapped` advisory
reads the ledger directly for the same reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 6:58am

Request Review

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/platform-objects.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/platform-objects)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 3, 2026 07:12
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit c44dd5e Aug 3, 2026
22 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4769-adr0104-attest-after-seed branch August 3, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants